package com.weareint.common;

import com.alibaba.fastjson.JSONObject;
import com.weareint.common.config.ApplicationProperties;
import com.weareint.common.constants.Constants;
import com.weareint.common.constants.RedisKeys;
import com.weareint.common.exceptions.SystemRuntimeException;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Lists;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
public class TokenProvider {

    public static final String AUTHORIZATION = "Authorization";
    private static final String TOKEN_PREFIX = "Bearer ";
    private Key key;
    private long tokenValidityInSeconds;
    private long tokenValidityInSecondsForRememberMe;

    private final LoadingCache<Long, HashSet<String>> apiCaches =
            CacheBuilder.newBuilder()
                    .expireAfterWrite(3, TimeUnit.SECONDS)
                    .build(
                            new CacheLoader<Long, HashSet<String>>() {
                                @Override
                                public HashSet<String> load(Long userId) throws Exception {
                                    return readHashSetFromRedis(
                                            RedisKeys.USER_APIS_KEY_PREFIX + userId);
                                }
                            });

    private final LoadingCache<Long, HashSet> roleCodeCaches =
            CacheBuilder.newBuilder()
                    .expireAfterWrite(3, TimeUnit.SECONDS)
                    .build(
                            new CacheLoader<Long, HashSet>() {
                                @Override
                                public HashSet load(Long userId) throws Exception {
                                    return readHashSetFromRedis(
                                            RedisKeys.USER_ROLES_CODE_KEY_PREFIX + userId);
                                }
                            });

    /**
     * <pre>
     *     用于存储本线程已登录的用户ID
     * </pre>
     */
    private static final ThreadLocal<Long> local = new ThreadLocal<>();

    @Autowired private ApplicationProperties applicationProperties;

    @Autowired private StringRedisTemplate redisTemplate;

    @PostConstruct
    public void init() {
        ApplicationProperties.JwtConfig jwtConfig = applicationProperties.getJwt();
        String secret = jwtConfig.getSecret();
        this.tokenValidityInSeconds = 1000 * jwtConfig.getTokenValidityInSeconds();
        this.tokenValidityInSecondsForRememberMe =
                1000 * jwtConfig.getTokenValidityInSecondsForRememberMe();
        key = new SecretKeySpec(new Base64().decode(secret), "HmacSHA512");
    }

    /**
     * <pre>
     *      依据用户ID生成Jwt Token
     * </pre>
     * @param uid
     * @return
     */
    public String createToken(Long uid, List<String> apis, Set<String> roleCodes) {
        return createToken(uid, false, apis, roleCodes);
    }

    /**
     * <pre>
     *      依据用户ID生成Jwt Token
     * </pre>
     * @param uid
     * @param rememberMe
     * @return
     */
    public String createToken(
            Long uid, boolean rememberMe, List<String> apis, Set<String> roleCodes) {
        Date validity;
        if (rememberMe) {
            validity =
                    new Date(System.currentTimeMillis() + this.tokenValidityInSecondsForRememberMe);
        } else {
            validity = new Date(System.currentTimeMillis() + this.tokenValidityInSeconds);
        }
        String token =
                TOKEN_PREFIX
                        + Jwts.builder()
                                .setSubject(String.valueOf(uid))
                                .signWith(SignatureAlgorithm.HS512, key)
                                .setExpiration(validity)
                                .compact();
        Duration duration =
                Duration.ofMillis(
                        rememberMe
                                ? this.tokenValidityInSecondsForRememberMe
                                : this.tokenValidityInSeconds);
        // 把Token也向缓存中保存一份，如果缓存中的Token消失了，那也就表示过期了
        redisTemplate.opsForValue().set(getCacheKey(uid), token, duration);
        if (null != apis && !apis.isEmpty()) {
            redisTemplate
                    .opsForValue()
                    .set(
                            RedisKeys.USER_APIS_KEY_PREFIX + uid,
                            JSONObject.toJSONString(apis),
                            duration);
        }
        if (null != roleCodes && !roleCodes.isEmpty()) {
            redisTemplate
                    .opsForValue()
                    .set(
                            RedisKeys.USER_ROLES_CODE_KEY_PREFIX + uid,
                            JSONObject.toJSONString(roleCodes),
                            duration);
        }
        return token;
    }

    /**
     * <pre>
     *      从HttpServletRequest中获取User ID
     * </pre>
     * @param request
     * @return
     */
    public Long getUserId(HttpServletRequest request) {
        return getUserId(getTokenFromRequest(request));
    }

    /**
     * <pre>
     *      从Token中获取User ID
     * </pre>
     * @param token
     * @return
     */
    public Long getUserId(String token) {
        if (null == token) {
            return null;
        }
        token = token.replace(TOKEN_PREFIX, "").trim();
        Claims claims = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
        return Long.valueOf(claims.getSubject());
    }

    /**
     * <pre>
     *      从HttpServletRequest中获取Jwt Token
     * </pre>
     * @param request
     * @return
     */
    public String getTokenFromRequest(HttpServletRequest request) {
        String authorization = request.getHeader(AUTHORIZATION);
        if (StringUtils.isBlank(authorization)
                || !StringUtils.startsWith(authorization, TOKEN_PREFIX)) {
            return null;
        }
        return authorization;
    }

    /**
     * <pre>
     *      检查Jwt Token是否正确
     * </pre>
     * @param request
     * @return
     */
    public boolean checkToken(HttpServletRequest request) {
        String uri = request.getRequestURI();
        String token = getTokenFromRequest(request);
        if (null == token) {
            return false;
        }
        try {
            Long userId = getUserId(token);
            // 与Redis中的Token进行比对
            String cachedToken = redisTemplate.opsForValue().get(getCacheKey(userId));
            if (!Objects.equals(cachedToken, token)) {
                return false;
            }

            // 不是超级管理员，而且没有访问此Api的权限
            if (!isSuperAdmin(userId) && !checkApi(userId, uri)) {
                return false;
            }
            // 记录用户在线状态
            redisTemplate
                    .opsForZSet()
                    .add(
                            RedisKeys.USER_ONLINE_KEY_PREFIX,
                            String.valueOf(userId),
                            System.currentTimeMillis());
            local.set(userId);
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    public void removeUserIdFromThreadLocal() {
        local.remove();
    }

    public Long getCurrentUserIdFromThreadLocal() {
        return null == local.get() ? -1L : local.get();
    }

    public Long getCurrentUserIdFromThreadLocal(boolean thrExOnNotFound) {
        Long id = getCurrentUserIdFromThreadLocal();
        if (thrExOnNotFound && id == -1L) {
            throw new SystemRuntimeException("Not found current user id.");
        }
        return id;
    }

    public void invalidToken() {
        invalidToken(getCurrentUserIdFromThreadLocal());
    }

    public void invalidToken(Long userId) {
        if (null != userId) {
            // 删除缓存的Token、Apis、RoleCodes
            redisTemplate.delete(
                    Lists.newArrayList(
                            getCacheKey(userId),
                            RedisKeys.USER_APIS_KEY_PREFIX + userId,
                            RedisKeys.USER_ROLES_CODE_KEY_PREFIX + userId));
            // 删除在线状态
            redisTemplate
                    .opsForZSet()
                    .remove(RedisKeys.USER_ONLINE_KEY_PREFIX, String.valueOf(userId));
        }
    }

    private boolean checkApi(Long userId, String api) {
        if (null == userId) {
            return false;
        }
        try {
            HashSet<String> apis = apiCaches.get(userId);
            return null != apis && apis.contains(api);
        } catch (ExecutionException e) {
            if (log.isErrorEnabled()) {
                log.error("Check Api from local cache , error!!!", e);
            }
        }
        return false;
    }

    public boolean isSuperAdmin(Long userId) {
        if (null == userId) {
            return false;
        }
        try {
            HashSet<String> roleCodes = roleCodeCaches.get(userId);
            return null != roleCodes && roleCodes.contains(Constants.AUTH.SUPER_ADMIN);
        } catch (ExecutionException e) {
            if (log.isErrorEnabled()) {
                log.error("Check Role [super admin] error.", e);
            }
        }
        return false;
    }

    private String getCacheKey(Long userId) {
        return RedisKeys.USER_AUTHED_KEY_PREFIX + userId;
    }

    private HashSet<String> readHashSetFromRedis(String key) {
        String value = redisTemplate.opsForValue().get(key);
        if (null == value) {
            return new HashSet<>();
        }
        return new HashSet<>(JSONObject.parseArray(value, String.class));
    }
}
